06 设计模式——策略模式

返回设计模式博客目录

介绍


策略模式:策略模式定义了一系列算法,并将每一个算法封装起来,而且使它们可以相互替换。策略模式让算法独立于使用它的客户端而独立变化

在软件开发中常常遇到这样的情况:实现某一功能可以有多种算法或者策略,我们根据实际情况选择不同的算法或者策略来完成该功能。例如,排序算法,可以使用插入排序、归并排序、冒泡排序等。

针对这种情况,一种常规的方法是将多种算法写到一个类中。例如,需要提供多种排序算法,可以将这些算法写到一个类中,每一个方法对应一个具体的排序算法;当然,也可以将这些排序算法封装在一个统一的方法中,通过 if…else… 或者 case 等条件判断语句来选择具体的算法。这两种实现方法我们都可以称为硬编码。然而,当很多算法集中在一个类中时,这个类就会变得臃肿,维护成本会变高,在维护时也更容易引发错误。如果我们需要增加一种新的排序算法,需要修改封装算法类的源代码。这就明显违反了开闭原则和单一职责原则。

如果将这些算法或者策略抽象出来,提供一个统一的接口,不同的算法或者策略有不同的实现类,这样在程序客户端就可以通过注入不同的实现对象来实现算法或者策略的动态替换,这种模式的可扩展性、可维护性也就更高,也就是我们要说的策略模式

优点

  • 结构清晰明了、使用简单直观;
  • 耦合度相对而言较低,扩展方便;
  • 操作封装也更为彻底,数据更为安全。

缺点

  • 随着策略的增加,子类也会变得繁多。

使用场景

  • 针对同一类型问题的多种处理方式,仅仅是具体行为有差别时。
  • 需要安全地封装多种同一类型的操作时。
  • 出现同一抽象类有多个子类,而又需要使用 if-else 或者 switch-case 来选择具体子类时。

UML 关系图

上图中的角色介绍:

  • Context:用来操作策略的上下文环境;
  • Strategy:策略的抽象;
  • ConcreteStrategyA、ConcreteStrategyB:具体的策略实现。

示例


下面我们以计算不同交通工具的车费来简单看看策略模式的实现。

常规实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public class PriceCalculator {
// 公交车类型
private static final int BUS = 1;
// 地铁类型
private static final int SUBWAY = 2;
public int calculatePrice(int km, int type) {
if (type == BUS) {
return busPrice(km);
} else if (type == SUBWAY) {
return subwayPrice(km);
}
return 0;
}
/**
* 十公里之内一元,超过十公里之后每加一元钱可以乘 5 公里
* @param km 公里数
* @return 公交车车费
*/
private int busPrice(int km) {
// 超过十公里的总距离
int extraTotal = km - 10;
// 超过的距离是 5 公里的倍数
int extraFactor = extraTotal / 5;
// 超过的距离对 5 公里取余
int fraction = extraTotal % 5;
// 价格计算
int price = 1 + extraFactor * 1 ;
return fraction > 0 ? ++price : price;
}
/**
* 6 公里(含)内 3 元;6 ~ 12 公里(含)4 元;
* 12 ~ 22 公里(含)5元;22 ~ 32 公里(含)6元;
* @param km 公里数
* @return 地铁车费
*/
private int subwayPrice(int km) {
if (km <= 6) {
return 3;
} else if (km <= 12) {
return 4;
} else if (km <= 22) {
return 5;
} else if (km <= 32) {
return 6;
}
// 其他距离简化为 7 元
return 7;
}
}

PriceCalculator 类很明显地问题就是并不是单一职责,首先他承担了计算公交车和地铁乘坐价格的职责;另一个问题就是通过 if-else 的形式来判断使用哪种计算形式。当我们增加一种出行方式时,如出租车,那么我们就需要在 PriceCalculator 中增加一个方法来计算出租车出行的价格,并且再 calculatePrice(int km, int type) 函数中增加一个判断,代码添加后大致如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class PriceCalculator {
// 公交车类型
private static final int BUS = 1;
// 地铁类型
private static final int SUBWAY = 2;
// 出租车类型
private static final int TEXI = 3;
public int calculatePrice(int km, int type) {
if (type == BUS) {
return busPrice(km);
} else if (type == SUBWAY) {
return subwayPrice(km);
} else if (type == TEXI) {
return taxiPrice(km);
}
return 0;
}
/**
* 简单计算为每公里 2 元钱
* @param km 公里数
* @return 出租车费
*/
private int taxiPrice(int km) {
return km * 2;
}
// 其他代码
}

此时代码已经比较混乱,各种 if-else 缠绕其中。当价格的计算方法变化时,需要直接修改这个类中的代码,那么很可能有一段代码是其他几个计算方法所共同使用的,这就容易引入错误。另外,在增加出行方式时,需要手动修改,添加 if-else。这类代码必然是难以应对变化的,难以维护的。下面我们对上述示例用策略模式进行重构。

策略模式

首先需要定义一个抽象的价格计算接口。

1
2
3
4
5
6
7
8
public interface CalculateStrategy {
/**
* 按距离来计算价格
* @param km 公里数
* @return 价格
*/
int calculatePrice(int km);
}

对于每一种出行方式我们都有一个独立的计算策略类,这些策略类都实现了 CalculateStrategy 接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// 公交车价格计算策略
public class BusStrategy implements CalculateStrategy {
/**
* 十公里之内一元,超过十公里之后每加一元钱可以乘 5 公里
* @param km 公里数
* @return 公交车车费
*/
@Override
public int calculatePrice(int km) {
// 超过十公里的总距离
int extraTotal = km - 10;
// 超过的距离是 5 公里的倍数
int extraFactor = extraTotal / 5;
// 超过的距离对 5 公里取余
int fraction = extraTotal % 5;
// 价格计算
int price = 1 + extraFactor * 1 ;
return fraction > 0 ? ++price : price;
}
}
// 地铁价格计算策略
public class SubwayStrategy implements CalculateStrategy {
/**
* 6 公里(含)内 3 元;6 ~ 12 公里(含)4 元;
* 12 ~ 22 公里(含)5元;22 ~ 32 公里(含)6元;
* @param km 公里数
* @return 地铁车费
*/
@Override
public int calculatePrice(int km) {
if (km <= 6) {
return 3;
} else if (km <= 12) {
return 4;
} else if (km <= 22) {
return 5;
} else if (km <= 32) {
return 6;
}
// 其他距离简化为 7 元
return 7;
}
}

再创建一个扮演 Context 角色的类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class TrafficCalculator {
private CalculateStrategy mStrategy;
public CalculateStrategy getStrategy() {
return mStrategy;
}
public void setStrategy(CalculateStrategy strategy) {
this.mStrategy = strategy;
}
public static void main(String[] args) {
TrafficCalculator calculator = new TrafficCalculator();
calculator.setStrategy(new BusStrategy());
calculator.calculatePrice(16);
}
}

这种方案在隐藏实现的同时,可扩展性变得很强。例如,当我们需要增加出租车的计算策略时,只需要添加一个出租车计算策略类,然后将该策略设置给 TrafficCalculator。

1
2
3
4
5
6
7
8
9
10
11
// 出租车价格计算策略
public class TaxiStrategy implements CalculateStrategy {
/**
* 简单计算为每公里 2 元钱
* @param km 公里数
* @return 出租车费
*/
private int taxiPrice(int km) {
return km * 2;
}
}

将策略注入到 TrafficCalculator 中。

1
2
3
TrafficCalculator calculator = new TrafficCalculator();
calculator.setmStrategy(new TaxiStrategy());
calculator.calculatePrice(16);

模式在 ANDROID 中的应用


ANDROID 中属性动画的时间差值器有线性差值器、加速减速差值器等,这些差值器里面就用到了策略模式来隔离不同的动画速率计算算法。源码追踪关键方法依次为:

  • View:
    boolean draw(Canvas canvas, ViewGroup parent, long drawingTime)
    private boolean applyLegacyAnimation(ViewGroup parent, long drawingTime, Animation a, boolean scalingRequired)
  • Animation:
    public boolean getTransformation(long currentTime, Transformation outTransformation,float scale)
    public boolean getTransformation(long currentTime, Transformation outTransformation)
  • Interpolator:
    float getInterpolation(float input)

插值器在属性动画中扮演了很重要的角色。它将动画的速率计算封装到一个抽象中,也就是 Interpolator 中,该接口只有一个 getInterpolation(float input) 方法,通过这个方法来修改动画的流逝时间比,依次达到动画的加速、减速等效果,结构如下图所示。

Interpolator 就是这个计算策略的抽象,LinearInterpolator、CycleInterpolator 等插值器就是具体的实现策略,通过注入不同的插值器实现不同的动态效果。

策略模式实战


在我们加载图片项目 ImageLoader 中,加载请求会被封装成一个 Request 对象添加到请求队列中,ImageLoader 会为每个请求分配一个序列号,越晚加入的请求序列号值越大,默认情况下 ImageLoader 会按照先后顺序加载图片。但是现实中,我们可能需要最后添加到队列的请求先被执行。例如,在滚动 ListView 时,最后一项肯定是最晚被加载的,此时它却显示在屏幕上的,而其它优先被加载的请求却不在屏幕显示范围。当需求是在屏幕上显示的 Item View 的图片优先被加载,我们就需要 ImageLoader 支持从请求队列的尾部开始加载。也就是,这里至少需要两种策略。

依照策略模式,代码实现如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
* 加载策略接口
*/
public interface LoadPolicy {
int compare(BitmapRequest request1, BitmapRequest request2);
}
/**
* 顺序加载策略
*/
public class SerialPolicy implements LoadPolicy {
@Override
public int compare(BitmapRequest request1, BitmapRequest request2) {
return request1.serialNum - request2.serialNum;
}
}
/**
* 逆序加载策略
*/
public class SerialPolicy implements LoadPolicy {
@Override
public int compare(BitmapRequest request1, BitmapRequest request2) {
return request2.serialNum - request1.serialNum;
}
}
public class BitmapRequest implements Comparable<BitmapRequest> {
// 加载策略,默认顺序加载策略
private LoadPolicy mLoadPolicy = new SerialPolicy();
public void setLoadPolicy(LoadPolicy loadPolicy) {
this.mLoadPolicy = loadPolicy;
}
@Override
public int compareTo(BitmapRequest another) {
return mLoadPolicy.compare(this, another);
}
}